Start by loading necessary packages, including sqldf for EDA.

library(here)
## here() starts at C:/Users/wjrea/RProjects/constituencies
library(readr)
library(sf)
## Linking to GEOS 3.12.1, GDAL 3.8.4, PROJ 9.3.1; sf_use_s2() is TRUE
library(leaflet)
library(tmap)
## Breaking News: tmap 3.x is retiring. Please test v4, e.g. with
## remotes::install_github('r-tmap/tmap')
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(rvest)
## 
## Attaching package: 'rvest'
## The following object is masked from 'package:readr':
## 
##     guess_encoding
library(sqldf)
## Loading required package: gsubfn
## Loading required package: proto
## Loading required package: RSQLite

Read in constituency geometries for 2019 and 2024 (following boundary changes), downloaded from https://geoportal.statistics.gov.uk/.

uk_seats_2024 <- st_read(here("data",
                              paste0("Westminster_Parliamentary_Constituencies",
                                     "_July_2024_Boundaries_UK_BGC_",
                                     "-9160795013504052944.gpkg")))
## Reading layer `PCON_MAY_2024_UK_BGC' from data source 
##   `C:\Users\wjrea\RProjects\constituencies\data\Westminster_Parliamentary_Constituencies_July_2024_Boundaries_UK_BGC_-9160795013504052944.gpkg' 
##   using driver `GPKG'
## Simple feature collection with 650 features and 8 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -116.1487 ymin: 5352.6 xmax: 655653.8 ymax: 1220302
## Projected CRS: OSGB36 / British National Grid
uk_seats_2019 <- st_read(here("data",
                              paste0("WPC_Dec_2019_GCB_UK_2022_",
                                     "-3630186270451075951.gpkg")))
## Reading layer `WPC_Dec_2019_GCB_UK' from data source 
##   `C:\Users\wjrea\RProjects\constituencies\data\WPC_Dec_2019_GCB_UK_2022_-3630186270451075951.gpkg' 
##   using driver `GPKG'
## Simple feature collection with 650 features and 7 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -71.5668 ymin: 5350.361 xmax: 655653.8 ymax: 1220302
## Projected CRS: OSGB36 / British National Grid

Read in 2019 election results, downloaded from https://www.theyworkforyou.com/mps/, then join to constituency geometries.

mp_tib <- read_csv(here("data", "mps.csv"), show_col_types = FALSE)
uk_seats_2019[which(uk_seats_2019$pcon19nm == "Ynys Mon"), ]$pcon19nm <- "Ynys Môn"
uk_seats_2019a <- left_join(uk_seats_2019, mp_tib, join_by(pcon19nm == Constituency))

Read 2024 election results page, from the BBC.

ge_2024_results_page <- read_html("https://www.bbc.co.uk/news/election/2024/uk/constituencies")
containers <- ge_2024_results_page %>% html_nodes("div.ewuptu24")
no_seats <- length(containers)
constituencies <- rep(NA, length(containers))
winners <- rep(NA, length(containers))
for (i in 1:no_seats) {
  constituencies[i] <- containers[i] %>% html_nodes("a.ewuptu21") %>% html_text()
  tryCatch(
    {
      winners[i] <- containers[i] %>% html_nodes("div.ewuptu20") %>% html_text()
    },
    error = function(e) {
      cat(sprintf("Error in %s:", constituencies[i]), e$message, "\n")
    }
  )
}
results_2024_df <- data.frame(cbind(constituencies, winners))
colnames(results_2024_df) <- c("constituency", "party")
#  This was the last one in
results_2024_df[which(results_2024_df$constituency == "Inverness, Skye and West Ross-shire"),
                ]$party <- "Liberal Democrat gain from Scottish National Party"
results_2024_df$party <- gsub(" *(gain|hold|win)( .*|)$", "", results_2024_df$party)

#  Ensure constituency names match before joining dataframes
results_2024_df[which(results_2024_df$constituency == "Bridlington and the Wolds"),
                ]$constituency <- "Bridlington and The Wolds"
results_2024_df[which(results_2024_df$constituency == "Hull East"),
                ]$constituency <- "Kingston upon Hull East"
results_2024_df[which(results_2024_df$constituency == "Hull North & Cottingham"),
                ]$constituency <- "Kingston upon Hull North and Cottingham"
results_2024_df[which(results_2024_df$constituency == "Hull West & Haltemprice"),
                ]$constituency <- "Kingston upon Hull West and Haltemprice"
results_2024_df[which(results_2024_df$constituency == "South Holland and the Deepings"),
                ]$constituency <- "South Holland and The Deepings"

Modify party names in 2024 results where necessary, for colour list below.

results_2024_df[which(results_2024_df$party == "Alliance Party"),
                ]$party <- "Alliance"
results_2024_df[which(results_2024_df$party == "Sinn Fein"),
                ]$party <- "Sinn Féin"
results_2024_df[which(results_2024_df$party == "Social Democratic & Labour Party"),
                ]$party <- "Social Democratic and Labour Party"
results_2024_df[which(results_2024_df$party == "Democratic Unionist Party"),
                ]$party <- "DUP"
results_2024_df[which(results_2024_df$party == "Speaker of the House of Commons"),
                ]$party <- "Speaker"

Join geometry and results dataframes for 2024, on constituency name.

uk_seats_2024a <- left_join(uk_seats_2024, results_2024_df, join_by(PCON24NM == constituency))

Set up colours.

party_colours <- c("Conservative"                       = "#0087DC",
                   "Labour"                             = "#E4003B",
                   "Liberal Democrat"                   = "#FAA61A",
                   "Scottish National Party"            = "#FDF38E",
                   "Green"                              = "#02A95B",
                   "Labour/Co-operative"                = "#E4003B",
                   "Reform UK"                          = "#12B6CF",
                   "Sinn Féin"                          = "#326760",
                   "Alliance"                           = "#F6CB2F",
                   "DUP"                                = "#D46A4C",
                   "Plaid Cymru"                        = "#005B54",
                   "Social Democratic and Labour Party" = "#2AA82C",
                   "Alba"                               = "#005EB8",
                   "Workers Party"                      = "#780021",
                   "Speaker"                            = "black",
                   "Independent"                        = "#DCDCDC",
                   "Reclaim"                            = "#C03F31",
                   "Traditional Unionist Voice"         = "#0C3A6A",
                   "Ulster Unionist Party"              = "#48A5EE")

all_values_2019 <- unique(uk_seats_2019a$Party)
all_values_2024 <- unique(uk_seats_2024a$party)

# Assign default colours for unspecified values
default_palette_2019 <- tmaptools::get_brewer_pal("Set1", n = length(all_values_2019), contrast = c(0.2, 0.8))
## Warning in tmaptools::get_brewer_pal("Set1", n = length(all_values_2019), :
## contrast not used in qualitative color palettes

default_palette_2024 <- tmaptools::get_brewer_pal("Set1", n = length(all_values_2024), contrast = c(0.2, 0.8))
## Warning in tmaptools::get_brewer_pal("Set1", n = length(all_values_2024), :
## contrast not used in qualitative color palettes

# Combine custom colours with defaults
full_palette_2019 <- setNames(default_palette_2019, all_values_2019)
full_palette_2019[names(party_colours)] <- party_colours
full_palette_2024 <- setNames(default_palette_2024, all_values_2024)
full_palette_2024[names(party_colours)] <- party_colours

View 2019 interactive constituency map.

interactive_2019_map <- tm_shape(uk_seats_2019a) +
                        tm_polygons(col = "Party", palette = full_palette_2019,
                                    title = "Constituencies pre 2024 Election",
                                    id = "pcon19nm",
                                    popup.vars = c("Party" = "Party"))
tmap_leaflet(interactive_2019_map)